import dataclasses
import enum
import functools
import logging
from typing import Any

from sysreptor.utils.fielddefinition.types import BaseField, FieldDataType
from sysreptor.utils.utils import groupby_to_dict


class SortOrder(enum.StrEnum):
    ASC = 'asc'
    DESC = 'desc'


@functools.total_ordering
@dataclasses.dataclass
class SortKeyPart:
    value: Any
    order: SortOrder
    context: dict = dataclasses.field(default_factory=dict)

    def __hash__(self):
        return hash(self.value)

    def __eq__(self, other) -> bool:
        return self.value == other.value

    def __lt__(self, other) -> bool:
        if self.order == SortOrder.DESC:
            return other.value < self.value
        else:
            return self.value < other.value

    def __repr__(self) -> str:
        return f'SortKeyPart(value={self.value}, order={self.order})'


def format_sortable_field(value, definition: BaseField, cvss_by_level=False):
    if definition.type in [FieldDataType.OBJECT, FieldDataType.LIST, FieldDataType.USER]:
        logging.warning('Sorting by unsupported data type. Ignoring field.')
        return ''
    elif definition.type == FieldDataType.CVSS:
        if cvss_by_level:
            return value['level_number']
        else:
            return float(value['score'])
    elif definition.type == FieldDataType.CWE:
        if not value:
            return -1
        return int(value.replace('CWE-', ''))
    elif definition.type == FieldDataType.ENUM:
        # Sort enums by position of choice in choices list, not by value
        return next((i for i, c in enumerate(definition.choices) if c.value == value['value']), -1)
    elif value is not None:
        return value
    elif definition.type == FieldDataType.BOOLEAN:
        return False
    elif definition.type == FieldDataType.NUMBER:
        return 0
    # STRING, MARKDOWN, COMBOBOX, DATE, USER, JSON
    return ''


def sort_findings_by_fields(findings, project_type):
    def get_sort_key(finding):
        out = []
        for order_field_config in project_type.finding_ordering:
            out.append(SortKeyPart(
                value=format_sortable_field(
                    value=finding.get(order_field_config['field']),
                    definition=project_type.finding_fields_obj[order_field_config['field']]),
                order=SortOrder(order_field_config.get('order', 'asc'))))

        # Always sort by created as last key to ensure consistent ordering
        out.append(SortKeyPart(finding.get('created') or '', order=SortOrder.ASC))
        return tuple(out)

    return sorted(findings, key=get_sort_key)


def sort_findings_by_order(findings):
    return sorted(findings, key=lambda f: (f.get('order') or 0, f.get('created') or ''))


def sort_findings(findings, project_type, override_finding_order=False):
    if override_finding_order or not project_type.finding_ordering:
        return sort_findings_by_order(findings)
    else:
        return sort_findings_by_fields(findings, project_type)


def group_findings(findings, project_type, override_finding_order=False):
    def get_group_key(finding):
        # The group key is used for grouping and also sorting
        out = []
        for group_field_config in project_type.finding_grouping or []:
            value = finding.get(group_field_config['field'])
            definition = project_type.finding_fields_obj[group_field_config['field']]
            out.append(SortKeyPart(
                value=format_sortable_field(value=value, definition=definition, cvss_by_level=True),
                order=SortOrder(group_field_config.get('order', 'asc')),
                context={'value': value, 'definition': definition}))
        return tuple(out)

    def get_group_label(keys):
        if not keys:
            return ''

        v = keys[0].context['value']
        d = keys[0].context['definition']

        if v is None:
            return ''
        elif d.type == FieldDataType.ENUM:
            return v['label']
        elif d.type == FieldDataType.CVSS:
            return v['level']
        elif d.type == FieldDataType.BOOLEAN:
            return str(v).lower()
        else:
            return str(v)

    # Group findings
    # Sort groups by grouping fields
    groups_dict = groupby_to_dict(findings, key=get_group_key)

    # Create group data structure
    groups = [
        {
            'label': get_group_label(k),
            # Sort findings in groups
            'findings': sort_findings(findings=v, project_type=project_type, override_finding_order=override_finding_order),
        } for k, v in groups_dict.items()
    ]

    # Custom sorting: sort groups by order of fist finding in group
    if override_finding_order or not project_type.finding_ordering:
        groups = sorted(groups, key=lambda g: g['findings'][0].get('order') or 0)

    return groups
